/*
 * ============================================================================
 *
 *  Zombie:Reloaded
 *
 *  File:          models.inc
 *  Type:          Core
 *  Description:   Model manager.
 *
 *  Copyright (C) 2009  Greyscale, Richard Helgeby
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * ============================================================================
 */

/*
 * Note: Data structures and constants defined in models.h.inc.
 */

/**
 * Parsed model data.
 */
new ModelData[MODELS_MAX][ModelAttributes];

/**
 * Number of valid models.
 */
new ModelCount;


/**
 * Prepare all model/download data.
 */
ModelsLoad()
{
    new Handle:kvModels = INVALID_HANDLE;
    
    // Register config file.
    ConfigRegisterConfig(File_Models, Structure_List, CONFIG_FILE_ALIAS_MODELS);
    
    // Get models file path.
    decl String:modelPath[PLATFORM_MAX_PATH];
    new bool:exists = ConfigGetCvarFilePath(CVAR_CONFIG_PATH_MODELS, modelPath);
    
    // If file doesn't exist, then log and stop.
    if (!exists)
    {
        // Log failure and stop plugin.
        LogEvent(false, LogType_Fatal, LOG_CORE_EVENTS, LogModule_Models, "Config Validation", "Missing model list: \"%s\"", modelPath);
    }
    
    // Set the path to the config file.
    ConfigSetConfigPath(File_Models, modelPath);
    
    // Prepare key/value structure.
    kvModels = CreateKeyValues(CONFIG_FILE_ALIAS_MODELS);
    
    // Log what models file that is loaded.
    LogEvent(false, LogType_Normal, LOG_CORE_EVENTS, LogModule_Models, "Config Validation", "Loading models from file \"%s\".", modelPath);
    
    // Load model data file.
    FileToKeyValues(kvModels, modelPath);
    
    // Try to find the first model.
    KvRewind(kvModels);
    if (!KvGotoFirstSubKey(kvModels))
    {
        LogEvent(false, LogType_Fatal, LOG_CORE_EVENTS, LogModule_Models, "Config Validation", "Can't find any models in \"%s\"", modelPath);
    }
    
    decl String:buffer[256];
    decl String:name[64];
    decl String:path[PLATFORM_MAX_PATH];
    decl String:team[64];
    decl String:access[64];
    decl String:group[64];
    
    ModelCount = 0;
    new failedCount;
    new publicCount;
    new downloadCount;
    
    // Loop through all models and store attributes in ModelData array.
    do
    {
        if (ModelCount > MODELS_MAX)
        {
            // Maximum number of models reached. Log a warning and exit the loop.
            LogEvent(false, LogType_Error, LOG_CORE_EVENTS, LogModule_Models, "Config Validation", "Warning: Maximum number of models reached (%d). Skipping other models.", MODELS_MAX + 1);
            
            break;
        }
        
        KvGetString(kvModels, "name", name, sizeof(name));
        strcopy(ModelData[ModelCount][Model_Name], 64, name);
        
        KvGetString(kvModels, "path", path, sizeof(path));
        strcopy(ModelData[ModelCount][Model_Path], 64, path);
        
        KvGetString(kvModels, "team", team, sizeof(team));
        ModelData[ModelCount][Model_Team] = ModelsStringToTeam(team);
        
        KvGetString(kvModels, "access", access, sizeof(access));
        ModelData[ModelCount][Model_Access] = ModelsStringToAccess(access);
        
        KvGetString(kvModels, "group", group, sizeof(group));
        strcopy(ModelData[ModelCount][Model_Group], 64, group);
        
        
        // Validate model attributes.
        
        // Build path and check if model file exist.
        strcopy(buffer, sizeof(buffer), path);
        StrCat(buffer, sizeof(buffer), name);
        StrCat(buffer, sizeof(buffer), ".mdl");
        if (!FileExists(buffer))
        {
            LogEvent(false, LogType_Error, LOG_CORE_EVENTS, LogModule_Models, "Config Validation", "Warning: Invalid model name/path setting at index %d. File not found: \"%s\".", ModelCount + failedCount, buffer);
            failedCount++;
            continue;
        }
        
        // Validate team.
        if (ModelData[ModelCount][Model_Team] == ModelTeam_Invalid)
        {
            LogEvent(false, LogType_Error, LOG_CORE_EVENTS, LogModule_Models, "Config Validation", "Warning: Invalid model team setting at index %d: \"%s\".", ModelCount + failedCount, team);
            failedCount++;
            continue;
        }
        
        // Validate access.
        if (ModelData[ModelCount][Model_Access] == ModelAccess_Invalid)
        {
            LogEvent(false, LogType_Error, LOG_CORE_EVENTS, LogModule_Models, "Config Validation", "Warning: Invalid model access setting at index %d: \"%s\".", ModelCount + failedCount, access);
            failedCount++;
            continue;
        }
        else
        {
            // Increment public model counter for the current team.
            if (ModelData[ModelCount][Model_Access] == ModelAccess_Public)
            {
                publicCount++;
            }
        }
        
        // Validate group.
        if (ModelData[ModelCount][Model_Access] == ModelAccess_Group &&
            FindAdmGroup(group) == INVALID_GROUP_ID)
        {
            LogEvent(false, LogType_Error, LOG_CORE_EVENTS, LogModule_Models, "Config Validation", "Warning: Invalid model group setting at index %d. Couldn't find SourceMod group \"%s\".", ModelCount + failedCount, group);
            failedCount++;
            continue;
        }
        
        // Open directory with model files.
        new Handle:dir = OpenDirectory(path);
        
        // Check if failed.
        if (dir == INVALID_HANDLE)
        {
            LogEvent(false, LogType_Error, LOG_CORE_EVENTS, LogModule_Models, "Config Validation", "Error opening directory: %s", dir);
            continue;
        }
        
        // Reset file counter for the current model.
        downloadCount = 0;
        
        new FileType:type;
        decl String:file[64];
        decl String:fileShort[64];
        
        // Search for model files with the specified name and add them to
        // downloads table.
        while (ReadDirEntry(dir, file, sizeof(file), type))
        {
            // Skip if entry isn't a file.
            if (type != FileType_File)
            {
                continue;
            }
            
            // Find break point index in the string to get model name.
            // Add one to make space for null terminator.
            new breakpoint = FindCharInString(file, '.') + 1;
            strcopy(fileShort, breakpoint, file);
            
            // If this file doesn't match model name, then skip it.
            if (!StrEqual(name, fileShort, false))
            {
                continue;
            }
            
            // Format a full path string.
            strcopy(buffer, sizeof(buffer), path);
            Format(buffer, sizeof(buffer), "%s%s", buffer, file);
            
            AddFileToDownloadsTable(buffer);
            downloadCount++;
        }
        
        CloseHandle(dir);
        
        // Check if no model files were found.
        if (!downloadCount)
        {
            LogEvent(false, LogType_Error, LOG_CORE_EVENTS, LogModule_Models, "Config Validation", "Couldn't find any model files for \"%s\". Check name and path.", name);
        }
        else
        {
            ModelCount++;
        }
    } while (KvGotoNextKey(kvModels));
    
    CloseHandle(kvModels);
    
    // Check if there are no public models.
    if (!publicCount)
    {
        LogEvent(false, LogType_Fatal, LOG_CORE_EVENTS, LogModule_Models, "Config Validation", "Missing public model in \"%s\". There must be at least one public model.", modelPath);
    }
    
    // Precache models.
    ModelsPrecache();
    
    // Log model validation info.
    LogEvent(false, LogType_Normal, LOG_CORE_EVENTS, LogModule_Models, "Config Validation", "Successful: %d | Unsuccessful: %d", ModelCount, failedCount);
    
    // Set config data.
    ConfigSetConfigLoaded(File_Models, true);
    ConfigSetConfigReloadFunc(File_Models, GetFunctionByName(GetMyHandle(), "ModelsOnConfigReload"));
}

/**
 * Called when config is being reloaded.
 */
public ModelsOnConfigReload(ConfigFile:config)
{
    // Reload models config.
    ModelsLoad();
}

/**
 * Precaches all models.
 */
ModelsPrecache()
{
    decl String:file[PLATFORM_MAX_PATH];

    // Loop through all models, build full path and cache them.
    for (new model = 0; model < ModelCount; model++)
    {
        ModelsGetFullPath(model, file, sizeof(file));
        PrecacheModel(file);
    }
}

/**
 * Returns a random model index according to the specified filter settings.
 *
 * @param client                Optional. Client index used to check for
 *                              permissions in "group" or "admins" access mode.
 *                              Use negative index to disable permission check
 *                              (default).
 * @param teamFilter            Optional. Team filtering settings. Use 
 *                              ModelTeam_Invalid to disable filter. Default is
 *                              ModelTeam_Zombies.
 * @param accessRequireFlags    Optional. One or more required access flags.
 *                              Default is MODEL_ACCESS_PUBLIC.
 * @return              Random model index according to filter, or -1 on error.
 */
ModelsGetRandomModel(client = -1, ModelTeam:teamFilter = ModelTeam_Zombies, accessRequireFlags = MODEL_ACCESS_PUBLIC)
{
    decl modelIndexes[MODELS_MAX];
    new listCount;
    
    // Loop through all models.
    for (new index = 0; index < ModelCount; index++)
    {
        // Check team filtering. Skip if no match.
        if (teamFilter != ModelTeam_Invalid &&
            ModelsGetTeam(index) != teamFilter)
        {
            continue;
        }
        
        // Cache current model access flag.
        new ModelAccess:access = ModelsGetAccess(index);
        new accessFlag = ModelsGetAccessFlag(access);
        
        // Check access filtering. Skip if no match.
        if (accessRequireFlags > 0 &&
            !(accessRequireFlags & accessFlag))
        {
            continue;
        }
        
        // Do client group authentication if client is specified.
        if (client > 0)
        {
            // Check if current model use group authentication.
            if (access == ModelAccess_Group)
            {
                decl String:group[64];
                ModelsGetGroup(index, group, sizeof(group));
                
                if (!ZRIsClientInGroup(client, group))
                {
                    // Client not authorized to use this model.
                    continue;
                }
            }
            else
            {
                // No group authentication. Do regular authentication if model
                // is a admin model.
                if (access == ModelAccess_Admins &&
                    !ZRIsClientAdmin(client))
                {
                    // Client not authorized to use this model.
                    continue;
                }
            }
        }
        
        // Model passed filter tests. Add to list.
        modelIndexes[listCount] = index;
        listCount++;
    }
    
    // Check if any models passed the filter.
    if (listCount)
    {
        return modelIndexes[GetRandomInt(0, listCount - 1)];
    }
    else
    {
        return -1;
    }
}

/**
 * Validates the specified index according to maximum number of models, and
 * number of models in use. Unused indexes will fail validation by default.
 *
 * @param index         Model index to validate.
 * @param rangeOnly     Optional. Do not check if the index is in use. Default
 *                      is false, check if in use.
 * @return              True if valid, false otherwise.
 */
bool:ModelsIsValidIndex(index, bool:rangeOnly = false)
{
    new bool:rangeValid = (index >= 0 && index < MODELS_MAX);
    
    if (rangeOnly)
    {
        // Only check if the index is valid.
        return rangeValid;
    }
    else
    {
        // Check if the index is valid, and if it's in use.
        return rangeValid && (index < ModelCount);
    }
}

/**
 * Gets the name for the specified model.
 *
 * @param index     Model index.
 * @param buffer    Destination string buffer.
 * @param maxlen    Size of buffer.
 * @return          Number of cells written, or -1 on error.
 */
ModelsGetName(index, String:buffer[], maxlen)
{
    // Validate index.
    if (!ModelsIsValidIndex(index))
    {
        return -1;
    }
    
    return strcopy(buffer, maxlen, ModelData[index][Model_Name]);
}

/**
 * Gets the path for the specified model.
 *
 * @param index     Model index.
 * @param buffer    Destination string buffer.
 * @param maxlen    Size of buffer.
 * @return          Number of cells written, or -1 on error.
 */
ModelsGetPath(index, String:buffer[], maxlen)
{
    // Validate index.
    if (!ModelsIsValidIndex(index))
    {
        return -1;
    }
    
    return strcopy(buffer, maxlen, ModelData[index][Model_Path]);
}

/**
 * Gets the team for the specified model.
 *
 * @param index     Model index.
 * @return          Team for the specified model, ModelTeam_Invalid on error.
 */
ModelTeam:ModelsGetTeam(index)
{
    // Validate index.
    if (!ModelsIsValidIndex(index))
    {
        return ModelTeam_Invalid;
    }
    
    return ModelData[index][Model_Team];
}

/**
 * Gets the access setting for the specified model.
 *
 * @param index     Model index.
 * @return          Access setting for the specified model, ModelAccess_Invalid
 *                  on error.
 */
ModelAccess:ModelsGetAccess(index)
{
    // Validate index.
    if (!ModelsIsValidIndex(index))
    {
        return ModelAccess_Invalid;
    }
    
    return ModelData[index][Model_Access];
}

/**
 * Gets the access flag for the specified access setting.
 *
 * @param access    Access setting to convert.
 * @return          Access flag, or 0 on error.
 */
ModelsGetAccessFlag(ModelAccess:access)
{
    switch (access)
    {
        case ModelAccess_Public:
        {
            return MODEL_ACCESS_PUBLIC;
        }
        case ModelAccess_Admins:
        {
            return MODEL_ACCESS_ADMINS;
        }
        case ModelAccess_Hidden:
        {
            return MODEL_ACCESS_HIDDEN;
        }
        case ModelAccess_MotherZombies:
        {
            return MODEL_ACCESS_MOTHER_ZOMBIES;
        }
        case ModelAccess_Group:
        {
            return MODEL_ACCESS_GROUP;
        }
    }
    
    // Invalid access flag.
    return 0;
}

/**
 * Gets the group for the specified model.
 *
 * @param index     Model index.
 * @param buffer    Destination string buffer.
 * @param maxlen    Size of buffer.
 * @return          Number of cells written, or -1 on error.
 */
ModelsGetGroup(index, String:buffer[], maxlen)
{
    // Validate index.
    if (!ModelsIsValidIndex(index))
    {
        return -1;
    }
    
    return strcopy(buffer, maxlen, ModelData[index][Model_Group]);
}

/**
 * Gets the full model file path for the specified model.
 *
 * @param index     Model index.
 * @param buffer    Destination string buffer.
 * @param maxlen    Size of buffer.
 * @return          Number of cells written, or -1 on error.
 */
ModelsGetFullPath(index, String:buffer[], maxlen)
{
    decl String:path[PLATFORM_MAX_PATH];
    decl String:name[64];
    
    ModelsGetPath(index, path, sizeof(path));
    ModelsGetName(index, name, sizeof(name));
    
    buffer[0] = 0;
    
    StrCat(buffer, maxlen, path);
    StrCat(buffer, maxlen, name);
    StrCat(buffer, maxlen, ".mdl");
}

/**
 * Converts the specified string to a team setting.
 *
 * @param team  String to convert.
 * @return      Team setting, or ModelTeam_Invalid on error.
 */
ModelTeam:ModelsStringToTeam(const String:team[])
{
    if (StrEqual(team, "zombies", false))
    {
        return ModelTeam_Zombies;
    }
    else if (StrEqual(team, "humans", false))
    {
        return ModelTeam_Humans;
    }
    
    return ModelTeam_Invalid;
}

/**
 * Converts the specified class team ID to a team setting.
 *
 * @param teamid    Class team ID.
 * @return          Team setting, or ModelTeam_Invalid on error.
 */
ModelTeam:ModelsTeamIdToTeam(teamid)
{
    switch (teamid)
    {
        case ZR_CLASS_TEAM_ZOMBIES:
        {
            return ModelTeam_Zombies;
        }
        case ZR_CLASS_TEAM_HUMANS:
        {
            return ModelTeam_Humans;
        }
    }
    
    return ModelTeam_Invalid;
}

/**
 * Converts the specified string to a access setting.
 *
 * @param access    String to convert.
 * @return          Access setting, or ModelAccess_Invalid on error.
 */
ModelAccess:ModelsStringToAccess(const String:access[])
{
    if (StrEqual(access, "public", false))
    {
        return ModelAccess_Public;
    }
    else if (StrEqual(access, "admins", false))
    {
        return ModelAccess_Admins;
    }
    else if (StrEqual(access, "hidden", false))
    {
        return ModelAccess_Hidden;
    }
    else if (StrEqual(access, "motherzombies", false))
    {
        return ModelAccess_MotherZombies;
    }
    else if (StrEqual(access, "group", false))
    {
        return ModelAccess_Group;
    }
    
    return ModelAccess_Invalid;
}
